package cms.api.solr.index.service;

import cms.api.common.constant.SolrApiConstant;
import cms.api.common.utils.SolrClientUtils;
import cms.api.framework.search.IndexService;
import cms.api.framework.search.Indexable;
import cms.api.framework.search.ListQueryParams;
import cms.api.framework.search.PageQueryParams;
import cms.api.framework.search.QueryParams;
import cms.api.framework.search.SearchResultPage;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.schema.SchemaRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.client.solrj.response.schema.SchemaResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CommonParams;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Solr索引服务实现类
 *
 * @author heyingcheng
 * @email heyingcheng@ctvit.com.cn
 * @date 2019/6/29 18:00
 */
@Slf4j
public abstract class BaseSolrServiceImpl extends BaseIndexServiceImpl implements IndexService {

    @Autowired
    protected SolrClient solrClient;

    @Override
    public boolean isTypeExist(String index, String type) {
        log.info("判断索引库中的文档类型是否已存在, index=[{}], type=[{}]", index, type);
        return false;
    }

    @Override
    public String addDocument(String index, String type, Indexable document) {
        String docId = document.getDocId();
        Map documentObject = document.getDocumentObject();
        try {
            if (StringUtils.isNotBlank(docId)) {
                if (isDocumentExist(index, docId)) {
                    return docId;
                }
            } else {
                docId = this.generateDocId(getGeneratorKey4Index(index));
                documentObject.put(document.getDocIdFieldName(), docId);
            }
            addOrUpdateDocument(index, documentObject);
            solrClient.commit(index);
            return docId;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], document=[{}]", index, type, document.getDocumentObject(), e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], document=[{}]", index, type, document.getDocumentObject(), e);
        }
        return StringUtils.EMPTY;
    }

    @Override
    public long deleteDocumentById(String index, String type, String id) {
        try {
            UpdateResponse updateResponse = solrClient.deleteById(index, id);
            solrClient.commit(index);
            JSONObject responseHeaderObject = SolrClientUtils.formatNamedList(updateResponse.getResponseHeader());
            Integer status = responseHeaderObject.getInteger("status");
            if (Objects.nonNull(status) && status.equals(NumberUtils.INTEGER_ZERO)) {
                return NumberUtils.LONG_ONE;
            }
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], id=[{}]", index, type, id, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], id=[{}]", index, type, id, e);
        }
        return NumberUtils.LONG_ZERO;
    }

    /**
     * 从索引库中删除查询到的文档数据
     *
     * @param index 索引库名称
     * @param type 文档类型名称
     * @param queryParams 查询参数对象
     * @author heyingcheng
     * @date 2019/7/17 10:47
     * @return long Solr无法返回被删除的文档数目，返回0表示操作失败，返回1表示操作成功
     */
    @Override
    public long deleteDocumentByQuery(String index, String type, QueryParams queryParams) {
        Map<String, Object> conditions = queryParams.getConditions();
        if (MapUtils.isEmpty(conditions)) {
            throw new IllegalArgumentException("Conditions can not empty");
        }
        try {
            UpdateResponse updateResponse = solrClient.deleteByQuery(index, convertConditionsToString(conditions));
            solrClient.commit(index);
            JSONObject responseHeaderObject = SolrClientUtils.formatNamedList(updateResponse.getResponseHeader());
            Integer status = responseHeaderObject.getInteger("status");
            if (Objects.nonNull(status) && status.equals(NumberUtils.INTEGER_ZERO)) {
                return NumberUtils.LONG_ONE;
            }
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], queryParams=[{}]", index, type, JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], queryParams=[{}]", index, type, JSON.toJSONString(queryParams), e);
        }
        return NumberUtils.LONG_ZERO;
    }

    @Override
    public long updateDocument(String index, String type, Indexable document, boolean isUpsert) {
        String docId = document.getDocId();
        Map documentObject = document.getDocumentObject();
        try {
            if (isUpsert) {
                if (StringUtils.isBlank(docId)) {
                    docId = this.generateDocId(getGeneratorKey4Index(index));
                    documentObject.put(document.getDocIdFieldName(), docId);
                }
            } else {
                if (isDocumentExist(index, docId)) {
                    return NumberUtils.LONG_ZERO;
                }
            }
            addOrUpdateDocument(index, documentObject);
            solrClient.commit(index);
            return NumberUtils.LONG_ONE;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], document=[{}], isUpsert=[{}]", index, type, JSON.toJSONString(document), isUpsert, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], document=[{}], isUpsert=[{}]", index, type, JSON.toJSONString(document), isUpsert, e);
        }
        return NumberUtils.INTEGER_ZERO;
    }

    @Override
    public Map<String, Object> fetchDocumentById(String index, String type, String id, String... rtnFields) {
        try {
            return fetchDocumentById(index, id, (solrQuery) -> {
                if (ArrayUtils.isNotEmpty(rtnFields)) {
                    solrQuery.setFields(rtnFields);
                }
            });
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], id=[{}], rtnFields=[{}]", index, type, id, rtnFields, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], id=[{}], rtnFields=[{}]", index, type, id, rtnFields, e);
        }
        return Collections.emptyMap();
    }

    @Override
    public SearchResultPage search(PageQueryParams queryParams) {
        int startPage = queryParams.getStartPage();
        int pageSize = queryParams.getPageSize();
        try {
            SolrDocumentList docs = search(queryParams, startPage, pageSize);
            List<Map<String, Object>> documents = convertSolrDocumentListToMapList(docs);
            SearchResultPage searchResultPage = new SearchResultPage(startPage, pageSize, (int) docs.getNumFound(), documents);
            return searchResultPage;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        }
        return new SearchResultPage(startPage, pageSize, NumberUtils.INTEGER_ZERO, Collections.emptyList());
    }

    @Override
    public List<Map<String, Object>> search(ListQueryParams queryParams) {
        try {
            SolrDocumentList docs = search(queryParams, NumberUtils.INTEGER_ONE, Integer.MAX_VALUE);
            List<Map<String, Object>> documents = convertSolrDocumentListToMapList(docs);
            return documents;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        }
        return Collections.emptyList();
    }

    @Override
    public void setMapping(String index, String type, String mapping) {
    }

    @Override
    public List<Map<String, Object>> search(String index, String type, String queryString) {
        try {
            SolrQuery solrQuery = new SolrQuery();
            List<NameValuePair> pairs = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
            for (NameValuePair p : pairs) {
                solrQuery.set(p.getName(), p.getValue());
            }
            QueryResponse queryResponse = solrClient.query(index, solrQuery);
            SolrDocumentList docs = queryResponse.getResults();
            List<Map<String, Object>> documents = convertSolrDocumentListToMapList(docs);
            return documents;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], queryString=[{}]", index, type, queryString, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], queryString=[{}]", index, type, queryString, e);
        }
        return Collections.emptyList();
    }

    private UpdateResponse addOrUpdateDocument(String index, Map documentObject) throws SolrServerException, IOException {
        Set<Map.Entry<String, Object>> entries = documentObject.entrySet();
        SolrInputDocument doc = new SolrInputDocument();
        for (Map.Entry<String, Object> entry : entries) {
            doc.setField(entry.getKey(), entry.getValue());
        }
        return solrClient.add(index, doc);
    }

    private boolean isDocumentExist(String index, String id) throws IOException, SolrServerException {
        Map<String, Object> solrDocument = fetchDocumentById(index, id, null);
        if (MapUtils.isEmpty(solrDocument)) {
            return false;
        }
        return true;
    }

    private SolrDocumentList search(QueryParams queryParams, int startPage, int pageSize) throws SolrServerException, IOException {
        String index = queryParams.getIndex();
        if (StringUtils.isNotEmpty(index)) {
            throw new IllegalArgumentException("QueryParams.index can not empty");
        }

        Map<String, Object> conditions = queryParams.getConditions();
        if (MapUtils.isEmpty(conditions)) {
            throw new IllegalArgumentException("QueryParams.conditions can not empty");
        }

        SolrQuery solrQuery = new SolrQuery();
        solrQuery.set(CommonParams.Q, convertConditionsToString(conditions));

        String rtnFields = queryParams.getRtnFields();
        if (StringUtils.isNotEmpty(rtnFields)) {
            solrQuery.set(CommonParams.FL, rtnFields);
        }

        String highlightField = queryParams.getHighlightField();
        if (StringUtils.isNotEmpty(highlightField)) {
            solrQuery.addHighlightField(highlightField);
        }

        String sortField = queryParams.getSortField();
        if (StringUtils.isNotEmpty(sortField)) {
            solrQuery.set(CommonParams.SORT, sortField);
        }

        int start = (startPage - 1) * pageSize;
        solrQuery.setStart(start).setRows(pageSize);

        QueryResponse queryResponse = solrClient.query(index, solrQuery);
        SolrDocumentList docs = queryResponse.getResults();
        return docs;
    }

    private Map<String, Object> fetchDocumentById(String index, String id, Consumer<SolrQuery> consumer) throws IOException, SolrServerException {
        SchemaResponse.UniqueKeyResponse response = new SchemaRequest.UniqueKey().process(solrClient, index);
        String uniqueKey = response.getUniqueKey();
        SolrQuery solrQuery = new SolrQuery();
        solrQuery.set(CommonParams.Q, StringUtils.join(uniqueKey, SolrApiConstant.COLON, id));
        if (Objects.nonNull(consumer)) {
            consumer.accept(solrQuery);
        } else {
            solrQuery.setFields(uniqueKey);
        }
        QueryResponse queryResponse = solrClient.query(index, solrQuery);
        SolrDocumentList docs = queryResponse.getResults();
        List<Map<String, Object>> documents = convertSolrDocumentListToMapList(docs);
        if (CollectionUtils.isNotEmpty(documents)) {
            return documents.get(NumberUtils.INTEGER_ZERO);
        }
        return Collections.emptyMap();
    }

    private Set<String> convertConditionsToSet(Map<String, Object> conditions) {
        Set<String> querySet = new HashSet<>();
        Set<Map.Entry<String, Object>> entries = conditions.entrySet();
        entries.forEach(entry -> {
            querySet.add(entry.getKey().concat(SolrApiConstant.COLON).concat(String.valueOf(entry.getValue())));
        });
        return querySet;
    }

    private String convertConditionsToString(Map<String, Object> conditions) {
        Set<String> querySet = convertConditionsToSet(conditions);
        return StringUtils.join(querySet, SolrApiConstant.AND);
    }

    private List<Map<String, Object>> convertSolrDocumentListToMapList(SolrDocumentList docs) {
        if (CollectionUtils.isNotEmpty(docs)) {
            List<Map<String, Object>> documents = new ArrayList<>(docs.size());
            for (SolrDocument d : docs) {
                documents.add(convertSolrDocumentToMap(d));
            }
            return documents;
        }
        return Collections.emptyList();
    }

    private Map<String, Object> convertSolrDocumentToMap(SolrDocument solrDocument) {
        Map<String, Object> document = new HashMap<>(16);
        Set<Map.Entry<String, Object>> entries = solrDocument.entrySet();
        entries.forEach(entry -> {
            document.put(entry.getKey(), solrDocument.getFirstValue(entry.getKey()));
        });
        if (solrDocument.hasChildDocuments()) {
            List<SolrDocument> childDocuments = solrDocument.getChildDocuments();
            List<Map<String, Object>> childMapDocuments = new ArrayList<>(childDocuments.size());
            for (SolrDocument d : childDocuments) {
                childMapDocuments.add(convertSolrDocumentToMap(d));
            }
            document.put("childDocuments", childMapDocuments);
        }
        return document;
    }

}
